﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using GTServices.Entities;
using GTServices.Model.Extensions;
using GTServices.Model.Models;
using Caliburn.Micro;
using System.Data;
using System.Data.Entity;
using System.Reflection;
using System.Collections;
using io = System.IO;
using System.Runtime.Serialization;
using System.Xml.Serialization;
using System.Xml;
using System.Data.Objects;
using System.Collections.ObjectModel;
using System.Data.Entity.Migrations;
using System.ComponentModel.DataAnnotations;
namespace GTServices.Model.Models
{
    public class DataModel
    {

        //Core Tables
        public ObservableCollection<Category> Categories { get; set; }
        public ObservableCollection<Pattern> Patterns { get; set; }
        public ObservableCollection<FileExtension> FileExtensions { get; set; }
        public ObservableCollection<Entities.FileInfo> FileInfos { get; set; }
        public ObservableCollection<Language> Languages { get; set; }
        public ObservableCollection<Template> Templates { get; set; }
        public ObservableCollection<Classification> Classifications { get; set; }
       
        //Result Tables
        public ObservableCollection<MatchInfo> MatchInfos { get; set; }
        public ObservableCollection<GrepRequest> GrepRequests { get; set; }
        public ObservableCollection<GrepResult> GrepResults { get; set; }
        public ObservableCollection<Bug> Bugs { get; set; }

        public String CurrentContextPath { get; set; } 
        public DatabaseContext DatabaseContext { get; set; }

        public delegate void Resync(DataModel context); 

        public event Resync OnRsync;


        private Type[] knownTypes = { typeof(Category), 
                                      typeof(Pattern), 
                                      typeof(Classification), 
                                      typeof(Language),
                                      typeof(FileExtension),
                                      typeof(EntityCollection),
                                      typeof(Template),
                                      typeof(GrepResult)};



        public DataModel()
        {
            //No DataContext, 

            Init(); 
        }

        public void Init()
        {
            if (DatabaseContext != null)
            {
                Categories = DatabaseContext.Categories.Local;
                Patterns = DatabaseContext.Patterns.Local;
                FileExtensions = DatabaseContext.FileExtensions.Local;
                Languages = DatabaseContext.Languages.Local;
                Templates = DatabaseContext.Templates.Local;
                Classifications = DatabaseContext.Classifications.Local;

                MatchInfos = new BindableCollection<MatchInfo>();
                GrepRequests = new BindableCollection<GrepRequest>();
                GrepResults = new BindableCollection<GrepResult>();
                FileInfos = new BindableCollection<FileInfo>();
                Bugs = new BindableCollection<Bug>();

                // These arent synced to the db. 
                //MatchInfos = DatabaseContext.MatchInfos.Local;
                //FileInfos = DatabaseContext.FileInfos.Local;
                //GrepRequests = DatabaseContext.GrepRequests.Local;
                //GrepResults =DatabaseContext.GrepResults.Local;
                //Bugs = DatabaseContext.Bugs.Local;
            }
        }

        public DataModel(String databasePath)
        {
            DatabaseContext = new DatabaseContext(databasePath, "GrepTool" , false);
            Init();
        }

        public IList<PropertyInfo> GetChildEntities(Object o)
        {
            var entityProperties = o.GetType().GetProperties().Where(x => x.DeclaringType.BaseType == typeof(BaseEntity));
            return entityProperties.ToList(); 
        }

      

        public BaseEntity UpdateEntity(Object from)
        {
            return null;
        }

        public bool SaveState()
        {
            try
            {
                this.DatabaseContext.SaveChanges();
                return true;
            }
            catch (Exception e)
            {
                throw e;
            }
        }

        /// <summary>
        /// This is quite the method, but it works. It relies on the fact that Added Entities do not contain Cycles when serialized. 
        /// This is done using the IgnoreDataMember attribute to ignore Many<->Many relationships. By not cycling we don't have to maintain references of what's been added and  , and secondly, we can 
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        public BaseEntity CascadeAdd(BaseEntity item, bool updateOnExists = true, int depth = 0)
        {

            //if (depth > 3)
               // return item;

            //First we need to get all the Public Properties with a setter. 
            PropertyInfo[] infos = item.GetType().GetProperties(BindingFlags.Public | BindingFlags.SetField | BindingFlags.Instance);
           
            foreach (PropertyInfo info in infos)
            {
                if (info.GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(IgnoreDataMemberAttribute)) != null)
                {
                    continue;
                }
                if (ReflectionUtilities.InheritsFrom(info.PropertyType, typeof(BaseEntity)))
                {
                        Object value = info.GetValue(item, null);
                        if (value != null)
                        {
                            BaseEntity tmp = CascadeAdd(value as BaseEntity, updateOnExists, depth++);
                            info.SetValue(item, tmp , null);
                            depth--;
                        }
                }
                else if (ReflectionUtilities.IsIListOfGenericType(info.PropertyType, typeof(BaseEntity)))
                {
                    IList l = (info.GetValue(item, null) as IList);

                    BindableCollection<BaseEntity> items = new BindableCollection<BaseEntity>(l.Cast<BaseEntity>());
                    if (l != null)
                    {
                        for (int i = 0; i < items.Count; i++)
                        {
                            l[i] = CascadeAdd(l[i] as BaseEntity, updateOnExists, depth++);
                            depth--; 
                        }
                    }
                    //l.Clear();
                    //items.Apply(x => l.Add(x));
                }  
            }

            return AddEntity(item, updateOnExists);
        }

        public BaseEntity AddEntity(BaseEntity item, bool updateIfExists)
        {
            //First we need to get all the Public Properties with a setter. 
            PropertyInfo[] infos = item.GetType().GetProperties(BindingFlags.Public | BindingFlags.SetField | BindingFlags.Instance);

            //Last but not least we need to check if the item is contained in a set. 
            DbSet set = DatabaseContext.Set(item.GetType());
            if (!set.Local.Contains(item))
            {
                //The DB doesnt contain the item, that's good! This is easy, we can just add it. 
                set.Local.Add(item);
            }
            else
            {
                //This part is lame. Maybe we can make it better?? It should atleast support Combined keys. 

                //Get the keys. 
                //TODO: Support Comined keys. 
                var pi = infos.Where(x =>
                {

                    return x.GetCustomAttributes(true).FirstOrDefault(y => y.GetType() == typeof(KeyAttribute)) != null;
                }).FirstOrDefault();

                object key = pi != null ? pi.GetValue(item, null) : null;
                if (key != null)
                {
                    BaseEntity dbItemReference = set.Find(key) as BaseEntity;
                    if (updateIfExists)
                    {
                        DatabaseContext.Entry(dbItemReference).CurrentValues.SetValues(item);
                        //Need to sync collections. 
                        foreach (PropertyInfo info in infos.Where(x => ReflectionUtilities.IsIListOfGenericType(x.PropertyType, typeof(BaseEntity))))
                        {
                            //If the info is the IgnoredataAttribute, we need to ignore it or it may mess up related references. 
                            if (info.GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(IgnoreDataMemberAttribute)) != null)
                            {
                                continue;
                            }


                            IList dbItemReferenceList = info.GetValue(dbItemReference, null) as IList;
                            IList itemList = info.GetValue(item, null) as IList;


                            if (dbItemReference != null &&
                                itemList != null)
                            {
                                if (itemList != dbItemReference)
                                {
                                    //Sync issues if i do it directly from the list.
                                    foreach (BaseEntity be in itemList)
                                    {
                                        if (!dbItemReferenceList.Contains(be))
                                        {
                                            dbItemReferenceList.Add(be);
                                        }
                                    }
                                }
                                else
                                {
                                    int i = 0; 
                                }
                            }
                           
                        }
                        return dbItemReference;
                    }
                    return dbItemReference;
                }
            }

            return item;
        }



        public BaseEntity TryAddEntity(BaseEntity entity, bool updateIfExists = true)
        {
            DbSet set = DatabaseContext.Set(entity.GetType());
            if (set == null)
            {
                //Throw error and log
                throw new ArgumentException("Context does not contain the baseEntity type: " + entity.GetType().Name); 

            }
            if (set.Local.Contains(entity) && updateIfExists)
            {
                UpdateEntity(entity);
                return entity;
            }
            else
            {
                BaseEntity result = null;

                try
                {
                    return result;
                }
                catch (Exception e)
                {
                    throw e;
                }
            }
        }


        public bool UpdateEntity(BaseEntity entity)
        {
            DbSet set = DatabaseContext.Set(entity.GetType());
            if (set == null)
            {
                throw new ArgumentException("Context does not contain the baseEntity type: " + entity.GetType().Name); 
            }
            BaseEntity updatedEntity = (BaseEntity)set.Attach((object)entity);
            if (updatedEntity != null)
            {
                DatabaseContext.Entry(entity).State = EntityState.Modified;
                return true;
            }

            return false;
        }

        public bool TryAddItemToList(object item)
        {
            IList list = GetListFor(item.GetType());
            
            if (list == null)
            {
                //TODO Log/Error
                return false; 
            }
            try
            {
                if (!list.Contains(item))
                {
                    list.Add(item);
                }
                else
                {
                    //TODO: Do we update? Throw? For the time being let's throw a ItemExists exception which can be caught in the UI and present a dialog to update the Current it with the new item's values. 
                    //I can do this with reflection and it will still be "clean"
                  

                }
                return true;
            }
            catch (Exception e)
            {
                //TODO: error log
                
            }
            return false;
        }
        public IList GetListFor(Type t)
        {
            PropertyInfo[] infos = this.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
            foreach (PropertyInfo info in infos)
            {
                if (info.PropertyType.IsGenericType &&
                    info.PropertyType.GetInterfaces().Contains(typeof(System.Collections.IList)) &&
                    info.PropertyType.GetGenericArguments().Length > 0 &&
                    info.PropertyType.GetGenericArguments()[0] == t
                    )
                {
                    object o = info.GetValue(this, null);
                    if (o != null)
                        return o as IList;
                }
            }
            return null;
        }

        public Object Create(string typeName)
        {
            Assembly entityAssembly = Assembly.GetAssembly(typeof(EntityUtilities));
            try
            {
                Type t = entityAssembly.GetTypes().FirstOrDefault(x => x.Name.Equals(typeName, StringComparison.InvariantCultureIgnoreCase));
                if(t != null){
                    return Create(t); 
                }
            }
            catch (Exception e)
            {
                //TODO: Log/Handle

            }
            return null;
        }
        public Object Create(Type t)
        {
            try
            {
               return (object)Activator.CreateInstance(t);
            }
            catch (Exception e)
            {
                //TODO: Log/Handle
            }
            return null;
        }

        public bool Create(Type t, out object item)
        {
            item = null;
            PropertyInfo[] infos = this.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
            foreach (PropertyInfo info in infos)
            {
                if (info.PropertyType.IsGenericType &&
                    info.PropertyType.GetInterfaces().Contains(typeof(System.Collections.ICollection)) && 
                    info.PropertyType.GetGenericArguments().Length > 0 &&
                    info.PropertyType.GetGenericArguments()[0] == t
                    )
                {
                    object o = info.GetValue(this, null);
                    if (o != null)
                        item = o; 
                }
            }
            return item != null;
        }


        private void SaveToContext<T>(IEnumerable<T> collection, DatabaseContext context)
        {
            if (context == null)
            {
                //TODO: Add proper logging. 
                throw new ArgumentException("Null context"); 
            }
            DbSet set = context.Set(typeof(T));
            if (set != null)
            {
                foreach (T t in collection)
                {
                    if (!set.Local.Contains(t))
                    {
                        set.Add(t);
                    }
                }
            }
            else
            {
                //TODO: This shouldn't happen, but if it does. Error/Log
            }
        }

        public void SaveStateTo(DatabaseContext context)
        {
            SaveToContext(this.Categories, context);
            SaveToContext(this.Patterns, context);
            SaveToContext(this.FileExtensions, context);
          
            SaveToContext(this.Languages, context);

            SaveToContext(this.Templates, context);
            SaveToContext(this.Classifications, context);


            //SaveToContext(this.FileInfos, context);
            //SaveToContext(this.MatchInfos, context);
            //SaveToContext(this.GrepRequests, context);
            //SaveToContext(this.GrepResults, context);
            //SaveToContext(this.Bugs, context);    
        }

     
      
        public bool SaveDatabaseAs(String fileName)
        {
            string fnameNoPath = System.IO.Path.GetFileNameWithoutExtension(fileName);


            using (DatabaseContext dc = new DatabaseContext(fileName, fnameNoPath))
            {
                try
                {
                    SaveStateTo(dc);
                    dc.SaveChanges();
                    return true;
                }
                catch(Exception e)
                {
                    throw e;
                    //TDOO: Log//Error?
                }
            }
        }
      

        public void LoadPatternsFromFile(string path, Language defaultlaguage = null)
        {

            if (io.File.Exists(path))
            {
                io.StreamReader sr = new io.StreamReader(io.File.Open(path, io.FileMode.Open));
                while (!sr.EndOfStream)
                {
                    string line = sr.ReadLine();
                    if (!String.IsNullOrWhiteSpace(line))
                    {
                        Pattern p = new Pattern() { PatternStr = line};
                        if (defaultlaguage != null)
                            p.Language = defaultlaguage;

                        this.Patterns.Add(p); 
                    }
                }
            }
        }

        public String GetEntityAsXml(object entity)
        {
            Type t = entity.GetType(); 
            try
            {
                var serializer = new DataContractSerializer(typeof(object), knownTypes, 1000000, false, false, null, new ProxyDataContractResolver());

                using (var stream = new io.MemoryStream())
                {
                    // This will also lazy load all details
                    serializer.WriteObject(stream, entity);
                    stream.Seek(0, io.SeekOrigin.Begin);
                    io.StreamReader sr = new io.StreamReader(stream);
                    string s = sr.ReadToEnd();
                    return s; 
                }
            }
            catch (Exception e)
            {
                //TODO: Throw exception. 
                return e.Message;
            }
        }

        public void Remove(GrepResult gr)
        {
            Remove(gr.GrepRequest);
            this.GrepResults.Remove(gr);
        }
        public void Remove(GrepRequest gr)
        {
            this.GrepRequests.Remove(gr);
        }
        public void Remove(Entities.FileInfo fi)
        {
            this.FileInfos.Remove(fi);
        }

        public Object DeserializeXml(string xml)
        {
            XmlReader reader = XmlReader.Create(new io.StringReader(xml)); 
            
            try
            {
                var serializer = new DataContractSerializer(typeof(object), knownTypes, 10000, false, true, null, new ProxyDataContractResolver());
                
                using (var stream = new io.MemoryStream())
                {
                    // This will also lazy load all details
                   return serializer.ReadObject(reader);
                   
                }
            }
            catch (Exception e)
            {
                //TODO: Throw exception. 
                throw e;
            }
            return null;
        }

    }
}
